/**
 * Copyright 2011 T. Spilman
 * 
 * Licensed under the Apache License, Version 2.0 (the "License"); you may not
 * use this file except in compliance with the License. You may obtain a copy of
 * the License at
 * 
 * http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
 * License for the specific language governing permissions and limitations under
 * the License.
 */

package com.jreg;

import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.LinkedHashMap;
import java.util.Map;

import org.apache.commons.exec.CommandLine;
import org.apache.commons.exec.DefaultExecuteResultHandler;
import org.apache.commons.exec.DefaultExecutor;
import org.apache.commons.exec.ExecuteException;
import org.apache.commons.exec.ExecuteResultHandler;
import org.apache.commons.exec.ExecuteWatchdog;
import org.apache.commons.exec.Executor;
import org.apache.commons.exec.OS;
import org.apache.commons.exec.PumpStreamHandler;

interface Operation {

	CommandLine toCommandLine();

	int execute(long timeout) throws ExecuteException, RegistryException;

	String getOutputString();

	String getErrString();

	static abstract class AbstractOperation implements Operation,
			ExecuteResultHandler {

		public static final String COMMAND = "REG";
		public static final String HELP = "/?";

		public static final int SUCCESS = 0;
		public static final int FAILURE = 1;

		protected PumpStreamHandler streamHandler;
		protected SynchronizedExecuteResultHandler resultHandler;

		private ByteArrayOutputStream out;
		private ByteArrayOutputStream err;

		protected AbstractOperation() {
			this.out = new ByteArrayOutputStream();
			this.err = new ByteArrayOutputStream();
			this.streamHandler = new PumpStreamHandler(out, err);
			this.resultHandler = new SynchronizedExecuteResultHandler();
		}

		@Override
		public abstract CommandLine toCommandLine();

		@Override
		public int execute(long timeout) throws ExecuteException,
				RegistryException {
			validateOS();

			Executor executor = createExecutor();
			prepareExecutor(executor, timeout);

			CommandLine cmdLine = toCommandLine();

			try {
				executor.execute(cmdLine);
			} catch (IOException e) {
				throw new RegistryException(e);
			}
			resultHandler.waitFor();

			System.err.println(cmdLine);
			System.err.println("exitValue = " + resultHandler.getExitValue());

			String output = getOutputString();
			try {
				parse(output);
			} catch (RegistryException e) {
				throw new RegistryException(e);
			}

			return resultHandler.getExitValue();
		}

		public static void validateOS() throws RegistryException {
			if (!OS.isFamilyWindows())
				throw new RegistryException(
						"incompatible operating system detected");
		}

		protected Executor createExecutor() {
			return new DefaultExecutor();
		}

		protected void prepareExecutor(Executor executor, long timeout) {
			executor.setStreamHandler(streamHandler);
			executor.setExitValue(SUCCESS);
			ExecuteWatchdog watchdog = new ExecuteWatchdog(timeout);
			executor.setWatchdog(watchdog);
		}

		@Override
		public void onProcessComplete(int exitValue) {
			resultHandler.onProcessComplete(exitValue);
		}

		protected void parse(String output) throws RegistryException {

		}

		@Override
		public void onProcessFailed(ExecuteException e) {
			resultHandler.onProcessFailed(e);
		}

		public String getOutputString() {
			validateResult();
			return out.toString();
		}

		public String getErrString() {
			validateResult();
			return err.toString();
		}

		private final void validateResult() throws IllegalStateException {
			if (!resultHandler.hasResult())
				throw new IllegalStateException("result is not ready");
		}

		private class SynchronizedExecuteResultHandler extends
				DefaultExecuteResultHandler {

			@Override
			public void onProcessComplete(int exitValue) {
				super.onProcessComplete(exitValue);
				synchronized (this) {
					notify();
				}
			}

			@Override
			public void onProcessFailed(ExecuteException e) {
				super.onProcessFailed(e);
				synchronized (this) {
					notify();
				}
			}

			@Override
			public void waitFor() {
				while (!hasResult()) {
					synchronized (this) {
						try {
							wait();
						} catch (InterruptedException e) {
							e.printStackTrace();
							break;
						}
					}
				}
			}

		}

	}

	static class SimpleOperation extends AbstractOperation {

		private String operation;
		private String keyName;

		protected SimpleOperation(String operation, String keyName) {
			super();
			setOperation(operation);
			setKeyName(keyName);
		}

		public String getOperation() {
			return operation;
		}

		private void setOperation(String operation) {
			if (operation == null)
				throw new IllegalArgumentException("operation must not be null");
			this.operation = operation;
		}

		public String getKeyName() {
			return keyName;
		}

		public void setKeyName(String keyName) {
			if (keyName == null)
				throw new IllegalArgumentException("keyName must not be null");
			this.keyName = keyName;
		}

		public CommandLine toCommandLine() {
			CommandLine cmdLine = new CommandLine(COMMAND);
			String operation = getOperation();
			cmdLine.addArgument(operation);

			String source = getKeyName();
			cmdLine.addArgument(source, true);

			return cmdLine;
		}

	}

	static class FileOperation extends SimpleOperation {

		private String fileName;

		protected FileOperation(String operation, String keyName,
				String fileName) {
			super(operation, keyName);
			setFileName(fileName);
		}

		public void setFile(File file) {
			if (file == null)
				throw new IllegalAccessError("file must not be null");
			String path = file.getAbsolutePath();
			setFileName(path);
		}

		public String getFileName() {
			return fileName;
		}

		public void setFileName(String fileName) {
			if (fileName == null)
				throw new IllegalArgumentException("fileName must not be null");
			this.fileName = fileName;
		}

		@Override
		public CommandLine toCommandLine() {
			CommandLine cmdLine = super.toCommandLine();
			String fileName = getFileName();
			cmdLine.addArgument(fileName, true);
			return cmdLine;
		}

	}

	static class QueryOperation extends AbstractOperation {

		public static enum Value {
			/**
			 * Queries for a specific registry key values. If omitted, all
			 * values for the key are queried.
			 * 
			 * Argument to this switch can be optional only when specified along
			 * with /f switch. This specifies to search in valuenames only.
			 */
			V("/v"),
			/**
			 * Queries for the default value or empty value name (Default).
			 */
			VE("/ve");

			private String arg;

			private Value(String arg) {
				this.arg = arg;
			}

			public String getArg() {
				return arg;
			}

		}

		public static final String OPERATION = "QUERY";

		public static final String DEFAULT_FIND = "*";
		public static final String DELIMITER = "    ";

		private static final int INDEX_NAME = 1;
		private static final int INDEX_TYPE = 2;
		private static final int INDEX_VALUE = 3;
		private static final int EXPECTED_LENGTH = 4;

		private String keyName;
		private Value value;
		private String valueName;
		private boolean recursive;

		private String data;
		private boolean keyNamesOnly;
		private boolean dataOnly;
		private boolean caseSensitive;
		private boolean exact;

		private Type type;
		private boolean verbose;
		private String separator;

		private Result result;

		QueryOperation(String keyName) {
			setKeyName(keyName);
		}

		public String getKeyName() {
			return keyName;
		}

		public void setKeyName(String keyName) {
			if (keyName == null)
				throw new IllegalArgumentException("keyName must not be null");
			this.keyName = keyName;
		}

		public Value getValue() {
			return value;
		}

		public void setValue(Value value) {
			if (value == null)
				throw new IllegalArgumentException("value must not be null");
			this.value = value;
		}

		public String getValueName() {
			return valueName;
		}

		public void setValueName(String valueName) {
			this.valueName = valueName;
		}

		public boolean isRecursive() {
			return recursive;
		}

		public void setRecursive(boolean recursive) {
			this.recursive = recursive;
		}

		public String getFindData() {
			return data;
		}

		public void setFindData(String data) {
			this.data = data;
		}

		public boolean isFindKeyNamesOnly() {
			return keyNamesOnly;
		}

		public void setFindKeyNamesOnly(boolean keyNamesOnly) {
			this.keyNamesOnly = keyNamesOnly;
		}

		public boolean isFindDataOnly() {
			return dataOnly;
		}

		public void setFindDataOnly(boolean dataOnly) {
			this.dataOnly = dataOnly;
		}

		public boolean isFindCaseSensitive() {
			return caseSensitive;
		}

		public void setFindCaseSensitive(boolean caseSensitive) {
			this.caseSensitive = caseSensitive;
		}

		public boolean isFindExact() {
			return exact;
		}

		public void setFindExact(boolean exact) {
			this.exact = exact;
		}

		public Type getType() {
			return type;
		}

		public void setType(Type type) {
			this.type = type;
		}

		public boolean isVerbose() {
			return verbose;
		}

		public void setVerbose(boolean verbose) {
			this.verbose = verbose;
		}

		public String getSeparator() {
			return separator;
		}

		public void setSeparator(String separator) {
			this.separator = separator;
		}

		public CommandLine toCommandLine() {
			CommandLine cmdLine = new CommandLine(COMMAND);
			cmdLine.addArgument(OPERATION);

			String keyName = getKeyName();
			cmdLine.addArgument(keyName, true);

			Value value = getValue();
			if (value != null) {
				String arg = value.getArg();
				switch (value) {
					case V:
						cmdLine.addArgument(arg);
						String valueName = getValueName();
						if (valueName == null)
							throw new IllegalStateException(
									"valueName must not be null");
						cmdLine.addArgument(valueName, true);
						break;
					case VE:
						cmdLine.addArgument(arg);
						break;
				}
			}

			if (isRecursive())
				cmdLine.addArgument("/s");

			String find = getFindData();
			if (find != null) {
				cmdLine.addArgument("/f");
				cmdLine.addArgument(find, true);
				if (isFindKeyNamesOnly())
					cmdLine.addArgument("/k");
				if (isFindDataOnly())
					cmdLine.addArgument("/d");
				if (isFindCaseSensitive())
					cmdLine.addArgument("/c");
				if (isFindExact())
					cmdLine.addArgument("/e");
			}

			Type type = getType();
			if (type != null) {
				cmdLine.addArgument("/t");
				String name = type.name();
				cmdLine.addArgument(name);
			}

			if (isVerbose())
				cmdLine.addArgument("/z");

			String separator = getSeparator();
			if (separator != null) {
				cmdLine.addArgument("/se");
				cmdLine.addArgument(separator);
			}

			return cmdLine;
		}

		public Result getResult() {
			if (result == null)
				throw new IllegalStateException();
			return result;
		}

		@Override
		protected void parse(String output) throws RegistryException {
			result = new Result();

			String lineSeparator = System.getProperty("line.separator");
			String[] lines = output.split(lineSeparator);
			RegistryKey key = null;
			for (String line : lines) {
				if (line.trim().isEmpty())
					continue;
				if (line.startsWith(DELIMITER)) {
					if (key == null)
						throw new RegistryException(
								"key name expected before value");
					parseValue(key, line);
				} else {
					key = new RegistryKey(line);
					result.put(key);
				}
			}
		}

		private void parseValue(RegistryKey key, String line)
				throws RegistryException {
			final Collection<RegistryValue> regValues = result.get(key);
			try {
				String[] values = line.split(DELIMITER);
				if (values.length != EXPECTED_LENGTH)
					throw new RegistryException("unexpected length "
							+ values.length);
				String name = values[INDEX_NAME];
				Type type = Type.valueOf(values[INDEX_TYPE]);
				Object value = type.parse(values[INDEX_VALUE]);
				RegistryValue regValue = new RegistryValue(key, name, type,
						value);
				regValues.add(regValue);
			} catch (IllegalArgumentException e) {
				throw new RegistryException(e);
			}
		}

		public static class Result {

			private Map<RegistryKey, Collection<RegistryValue>> map;

			private Result() {
				super();
				this.map = new LinkedHashMap<RegistryKey, Collection<RegistryValue>>();
			}

			public void put(RegistryKey key) {
				map.put(key, null);
			}

			public Collection<RegistryValue> get(RegistryKey key) {
				Collection<RegistryValue> values = map.get(key);
				if (values == null) {
					values = new ArrayList<RegistryValue>();
					map.put(key, values);
				}
				return values;
			}

			boolean containsKey(RegistryKey key) {
				return map.containsKey(key);
			}

			void remove(RegistryKey key) {
				map.remove(key);
			}

			public String toString() {
				return map.toString();
			}

		}

	}

	static class AddOperation extends AbstractOperation {

		public static enum Value {
			/**
			 * The value name, under the selected Key, to add.
			 */
			V("/v"),
			/**
			 * adds an empty value name (Default) for the key.
			 */
			VE("/ve");

			private String arg;

			private Value(String arg) {
				this.arg = arg;
			}

			public String getArg() {
				return arg;
			}

		}

		public static final String OPERATION = "ADD";

		private String keyName;
		private Value value;
		private String valueName;
		private Type type;
		private String separator;
		private Object data;
		private String preparedData;

		AddOperation(String keyName) {
			setKeyName(keyName);
		}

		public String getKeyName() {
			return keyName;
		}

		public void setKeyName(String keyName) {
			if (keyName == null)
				throw new IllegalArgumentException("keyName must not be null");
			this.keyName = keyName;
		}

		public Value getValue() {
			return value;
		}

		public void setValue(Value value) {
			if (value == null)
				throw new IllegalArgumentException("value must not be null");
			this.value = value;
		}

		public String getValueName() {
			return valueName;
		}

		public void setValueName(String valueName) {
			this.valueName = valueName;
		}

		public Type getType() {
			return type;
		}

		public String getSeparator() {
			return separator;
		}

		public void setSeparator(String separator) {
			this.separator = separator;
		}

		public Object getData() {
			return data;
		}

		public void setData(Type type, Object data) throws RegistryException {
			if (type == null)
				throw new IllegalArgumentException("type must not be null");
			else if (data == null)
				throw new IllegalArgumentException("data must not be null");
			this.type = type;
			this.data = data;
			this.preparedData = type.prepare(data);
		}

		public String getPreparedData() {
			return preparedData;
		}

		public CommandLine toCommandLine() {
			CommandLine cmdLine = new CommandLine(COMMAND);
			cmdLine.addArgument(OPERATION);

			String keyName = getKeyName();
			cmdLine.addArgument(keyName, true);

			Value value = getValue();
			if (value != null) {
				switch (value) {
					case V:
						String arg = value.getArg();
						cmdLine.addArgument(arg);
						String valueName = getValueName();
						if (valueName == null)
							throw new IllegalStateException(
									"valueName must not be null");
						cmdLine.addArgument(valueName, true);
						break;
					case VE:
						arg = value.getArg();
						cmdLine.addArgument(arg);
						break;
				}
			}

			Type type = getType();
			if (type != null) {
				cmdLine.addArgument("/t");
				String name = type.name();
				cmdLine.addArgument(name);
			}

			String separator = getSeparator();
			if (separator != null) {
				cmdLine.addArgument("/s");
				cmdLine.addArgument(separator);
			}

			Object data = getData();
			if (data != null) {
				cmdLine.addArgument("/d");
				String preparedData = getPreparedData();
				cmdLine.addArgument(preparedData);
			}

			// Force overwriting the existing registry entry without prompt.
			cmdLine.addArgument("/f");

			return cmdLine;
		}

	}

	static class DeleteOperation extends AbstractOperation {

		public static enum Value {

			/**
			 * delete named value
			 */
			V("/v"),
			/**
			 * delete the value of empty value name (Default).
			 */
			VE("/ve"),
			/**
			 * delete all values under this key.
			 */
			VA("/va");

			private String arg;

			private Value(String arg) {
				this.arg = arg;
			}

			public String getArg() {
				return arg;
			}

		}

		public static final String OPERATION = "DELETE";

		private String keyName;

		private Value value;
		private String valueName;

		DeleteOperation(String keyName) {
			setKeyName(keyName);
		}

		public String getKeyName() {
			return keyName;
		}

		public void setKeyName(String keyName) {
			if (keyName == null)
				throw new IllegalArgumentException("keyName must not be null");
			this.keyName = keyName;
		}

		public Value getValue() {
			return value;
		}

		public void setValue(Value value) {
			if (value == null)
				throw new IllegalArgumentException("value must not be null");
			this.value = value;
		}

		public String getValueName() {
			return valueName;
		}

		public void setValueName(String valueName) {
			this.valueName = valueName;
		}

		public CommandLine toCommandLine() {
			CommandLine cmdLine = new CommandLine(COMMAND);
			cmdLine.addArgument(OPERATION);

			String keyName = getKeyName();
			cmdLine.addArgument(keyName, true);

			Value value = getValue();
			if (value != null) {
				String arg = value.getArg();
				switch (value) {
					case V:
						cmdLine.addArgument(arg);
						String valueName = getValueName();
						if (valueName == null)
							throw new IllegalStateException(
									"valueName must not be null");
						cmdLine.addArgument(valueName, true);
						break;
					case VE:
						cmdLine.addArgument(arg);
						break;
					case VA:
						cmdLine.addArgument(arg);
						break;
				}
			}

			// Forces the deletion without prompt.
			cmdLine.addArgument("/f");

			return cmdLine;
		}

	}

	static class CopyOperation extends AbstractOperation {

		public static final String OPERATION = "COPY";

		private String sourceKeyName;
		private String destinationKeyName;
		private boolean copySub;

		CopyOperation(String sourceKeyName, String destinationKeyName) {
			setSourceKeyName(sourceKeyName);
			setDestinationKeyName(destinationKeyName);
		}

		public String getSourceKeyName() {
			return sourceKeyName;
		}

		public void setSourceKeyName(String sourceKeyName) {
			if (sourceKeyName == null)
				throw new IllegalArgumentException(
						"sourceKeyName must not be null");
			this.sourceKeyName = sourceKeyName;
		}

		public String getDestinationKeyName() {
			return destinationKeyName;
		}

		public void setDestinationKeyName(String destinationKeyName) {
			if (destinationKeyName == null)
				throw new IllegalArgumentException(
						"destinationKeyName must not be null");
			this.destinationKeyName = destinationKeyName;
		}

		public boolean isCopySubkeys() {
			return copySub;
		}

		public void setCopySubkeys(boolean b) {
			this.copySub = b;
		}

		public CommandLine toCommandLine() {
			CommandLine cmdLine = new CommandLine(COMMAND);
			cmdLine.addArgument(OPERATION);

			String source = getSourceKeyName();
			cmdLine.addArgument(source, true);

			String destination = getDestinationKeyName();
			cmdLine.addArgument(destination, true);

			if (isCopySubkeys())
				cmdLine.addArgument("/s");

			// Forces the copy without prompt.
			cmdLine.addArgument("/f");

			return cmdLine;
		}

	}

	static class SaveOperation extends FileOperation {

		public static final String OPERATION = "SAVE";

		SaveOperation(String keyName, String fileName) {
			super(OPERATION, keyName, fileName);
		}

		@Override
		public CommandLine toCommandLine() {
			CommandLine cmdLine = super.toCommandLine();
			// Force overwriting the existing file without prompt.
			cmdLine.addArgument("/y");
			return cmdLine;
		}

	}

	static class RestoreOperation extends FileOperation {

		public static final String OPERATION = "RESTORE";

		RestoreOperation(String keyName, String fileName) {
			super(OPERATION, keyName, fileName);
		}

	}

	static class LoadOperation extends FileOperation {

		public static final String OPERATION = "LOAD";

		LoadOperation(String keyName, String fileName) {
			super(OPERATION, keyName, fileName);
		}

	}

	static class UnloadOperation extends SimpleOperation {

		public static final String OPERATION = "UNLOAD";

		UnloadOperation(String keyName) {
			super(OPERATION, keyName);
		}

	}

	static class CompareOperation extends AbstractOperation {

		public static enum Value {
			/**
			 * compare named value
			 */
			V("/v"),
			/**
			 * compare the value of empty value name (Default).
			 */
			VE("/ve");

			private String arg;

			private Value(String arg) {
				this.arg = arg;
			}

			public String getArg() {
				return arg;
			}

		}

		/**
		 * When omitted, output only differences.
		 */
		public static enum Output {
			/**
			 * Output all of differences and matches.
			 */
			OA("/oa"),
			/**
			 * Output only differences.
			 */
			OD("/od"),
			/**
			 * Output only matches.
			 */
			OS("/os"),
			/**
			 * No output.
			 */
			ON("/on");

			private String arg;

			private Output(String arg) {
				this.arg = arg;
			}

			public String getArg() {
				return arg;
			}

			public static Output getDefault() {
				return OD;
			}

		}

		public static final String OPERATION = "COMPARE";

		public static int SUCCESS_IDENTICAL = 0;
		public static int SUCCESS_DIFFERENT = 2;

		private String keyName1, keyName2;
		private Value value;
		private String valueName;
		private Output output;
		boolean compareAll;

		public CompareOperation(String keyName1, String keyName2) {
			super();
			setKeyName1(keyName1);
			setKeyName2(keyName2);
		}

		public String getKeyName1() {
			return keyName1;
		}

		public void setKeyName1(String keyName) {
			if (keyName == null)
				throw new IllegalArgumentException("keyName must not be null");
			this.keyName1 = keyName;
		}

		public String getKeyName2() {
			return keyName2;
		}

		public void setKeyName2(String keyName) {
			if (keyName == null)
				throw new IllegalArgumentException("keyName must not be null");
			this.keyName2 = keyName;
		}

		public Value getValue() {
			return value;
		}

		public void setValue(Value value) {
			this.value = value;
		}

		public String getValueName() {
			return valueName;
		}

		public void setValueName(String valueName) {
			this.valueName = valueName;
		}

		public Output getOutput() {
			return output;
		}

		public void setOutput(Output output) {
			this.output = output;
		}

		public boolean isCompareAll() {
			return compareAll;
		}

		public void setCompareAll(boolean compareAll) {
			this.compareAll = compareAll;
		}

		public CommandLine toCommandLine() {
			CommandLine cmdLine = new CommandLine(COMMAND);
			cmdLine.addArgument(OPERATION);

			String keyName1 = getKeyName1();
			cmdLine.addArgument(keyName1, true);

			String keyName2 = getKeyName2();
			cmdLine.addArgument(keyName2, true);

			Value value = getValue();
			if (value != null) {
				switch (value) {
					case V:
						String arg = value.getArg();
						cmdLine.addArgument(arg);
						String valueName = getValueName();
						if (valueName == null)
							throw new IllegalStateException(
									"valueName must not be null");
						cmdLine.addArgument(valueName, true);
						break;
					case VE:
						arg = value.getArg();
						cmdLine.addArgument(arg);
						break;
				}
			}

			Output output = getOutput();
			if (output != null) {
				String arg = output.getArg();
				cmdLine.addArgument(arg);
			}

			if (isCompareAll())
				// Compare all subkeys and values.
				cmdLine.addArgument("/s");

			return cmdLine;
		}

		@Override
		protected void prepareExecutor(Executor executor, long timeout) {
			super.prepareExecutor(executor, timeout);
			int[] exitValues = {
					SUCCESS_IDENTICAL, SUCCESS_DIFFERENT
			};
			executor.setExitValues(exitValues);
		}

	}

	static class ExportOperation extends FileOperation {

		public static final String OPERATION = "EXPORT";

		ExportOperation(String keyName, String fileName) {
			super(OPERATION, keyName, fileName);
		}

		@Override
		public CommandLine toCommandLine() {
			CommandLine cmdLine = super.toCommandLine();
			// Force overwriting the existing file without prompt.
			cmdLine.addArgument("/y");
			return cmdLine;
		}

	}

	static class ImportOperation extends FileOperation {

		public static final String OPERATION = "IMPORT";

		ImportOperation(String keyName, String fileName) {
			super(OPERATION, keyName, fileName);
		}

	}

	static class FlagsOperation extends SimpleOperation {

		public static enum Flag {
			DONT_VIRTUALIZE, DONT_SILENT_FAIL, RECURSE_FLAG;
		}

		public static final String OPERATION = "FLAGS";

		public FlagsOperation(String operation, String keyName) {
			super(operation, keyName);
		}

		@Override
		public CommandLine toCommandLine() {
			return super.toCommandLine();
		}

	}

}